package info.pinlab.pinplayer.io;


import info.pinlab.pinplayer.cache.CacheLevel;
import info.pinlab.pinplayer.cache.CacheManager;
import info.pinlab.pinplayer.cache.CachedValue;
import info.pinlab.pinplayer.cache.Pointer;
import info.pinlab.pinplayer.model.ExtendedResource;
import info.pinlab.pinsound.WavClip;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import javax.sound.sampled.UnsupportedAudioFileException;

import org.apache.commons.codec.binary.Base64;
import org.apache.log4j.Logger;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

public class SimpleJsonAdapter implements JsonAdapterIF{
	public static Logger logger = Logger.getLogger(SimpleJsonAdapter.class);
	
	private static final String CLAZZ_TAG = "class";
	private static final String HASH_TAG = "hash";
//	private static Gson defaultGson = new GsonBuilder().create();
	private final Gson gson; 
	private static CacheManager cacheManager;
	private static final String propertyFileName = "classtags.property";
	
	
	private static final Map<String, Class<?>> tag2ClazzMap = new HashMap<String, Class<?>>(); 
	private static final Map<Class<?>, String> clazz2TagMap = new HashMap<Class<?>, String>(); 
	
	@Override
	public void addTag2Clazz (String tag, Class<?> clazz){
		tag2ClazzMap.put(tag, clazz);
		clazz2TagMap.put(clazz, tag);
	}

	
	public SimpleJsonAdapter(boolean noRefsInJson){
		final GsonBuilder gsonBuilder = new GsonBuilder();
		gsonBuilder.disableHtmlEscaping();
		gsonBuilder.setPrettyPrinting();
		gsonBuilder.registerTypeAdapter(ExtendedResource.class, new ExtendedResourceAdapter());
		gsonBuilder.registerTypeAdapter(WavClip.class, new WavAdapter());
		gsonBuilder.registerTypeAdapter(CachedValue.class, new CachedValueAdapter(noRefsInJson));
		gson = gsonBuilder.create();
		
		initClassTags();
	}

	
	private void initClassTags(){
		InputStream is = this.getClass().getResourceAsStream(propertyFileName);
		if(is!=null){
			Properties props = new Properties();
			String clazzFQN = "";
			String clazzTag = "";
			try {
				props.load(is);
				for(Object key : props.keySet()){
					clazzTag = (String) key;
					clazzFQN =  props.getProperty(clazzTag);
//					System.out.println(key+"\t"+clazzFQN);
					Class<?> clazz = Class.forName(clazzFQN);
					this.addTag2Clazz(clazzTag, clazz);
				}
			} catch (IOException e) {
				logger.warn("Couldn't find class in classpath '" + clazzFQN +"'");
				e.printStackTrace();
			} catch (ClassNotFoundException e) {
				
				e.printStackTrace();
			}
		}else{
			logger.warn("Couldn't find jarred class2tag property file '" + propertyFileName +"'");
		}
	}
	
	public void setCacheManager(CacheManager cacheManager){
		SimpleJsonAdapter.cacheManager = cacheManager;
	}
	
	
	private static class CachedValueAdapter<T> implements
	JsonSerializer<CachedValue<?>>,
	JsonDeserializer<CachedValue<T>>{

		private final boolean useRefs ;
		
		private CachedValueAdapter(/*CacheLevel2 level, CacheManager manager,*/ boolean noRefsInJson){
			this.useRefs = !noRefsInJson;
		}

		@SuppressWarnings("unchecked")
		@Override
		public CachedValue<T> deserialize(JsonElement json, Type type,
				JsonDeserializationContext context) throws JsonParseException{
			final ParameterizedType typeArg = (ParameterizedType)type;
			final Type paramType = typeArg.getActualTypeArguments()[0];
			final Class<?> clazz = (Class<?>)paramType;

			if(json.isJsonPrimitive()){ //-- it is a reference!
				final String refId = json.getAsString();
				return CachedValue.getCacheWrapperForValue(cacheManager.get(refId, (Class<T>)clazz), (Class<T>)clazz);
			}else{ //-- not a reference but json comes!
//					System.out.println("CachedValue : "+ clazz  +"\n" + json);
					Object obj = null;
					if(ExtendedResource.class.isAssignableFrom(clazz)){
						obj = context.deserialize(json, ExtendedResource.class);
					}else{
						obj = context.deserialize(json, clazz);
					}
					return CachedValue.getCacheWrapperForValue((T)obj, (Class<T>)clazz);
			}
		}

		@Override
		public JsonElement serialize(CachedValue<?> cached, Type typeOfT,
				JsonSerializationContext context){
//			System.out.println("Deserialize Cached!");
			
			final Object obj = cached.getValue();

			Class<?> clazz = null; 
			if(obj instanceof ExtendedResource){
				//-- use the interface class to serialize! --//
				clazz = ((ExtendedResource<?>) obj).getInterfaceClass();
			}else{
				clazz = obj.getClass();
			}
			CacheLevel cacheLevel = cacheManager.getCacheLevel();
			Pointer ptr = cacheManager.unsafeCache(cacheLevel, obj, clazz);
			
			if(useRefs){
				return context.serialize(cacheLevel.getPrefix() + "://"	 + ptr.toString());
			}else{
//				System.out.println("No refs " + clazz);
				if(ExtendedResource.class.isAssignableFrom(clazz)){
					return context.serialize(obj, ExtendedResource.class);
				}else{
					return context.serialize(obj);
				}
			}
		}
	}
	
	
	private static class WavAdapter implements 
	JsonSerializer<WavClip>,
	JsonDeserializer<WavClip>{
		@Override
		public JsonElement serialize(WavClip wav, Type t,
				JsonSerializationContext context){
			String wavAsString = new String(Base64.encodeBase64(wav.toWavFile()));
			JsonObject obj = new JsonObject();
			obj.addProperty("file", wavAsString);
			obj.addProperty("frames", wav.getDurInFrames());
			obj.addProperty("hz", wav.getAudioFormat().getFrameRate());
			obj.addProperty("hash", wav.hashCode());
			return obj;
		}
		
		@Override
		public WavClip deserialize(JsonElement json, Type t,
				JsonDeserializationContext context) throws JsonParseException{
			JsonObject jobj =  json .getAsJsonObject();
			String wavfile = jobj.get("file").getAsString();
			byte [] stringBytes = wavfile.getBytes();
			
//			byte [] stringBytes = json.getAsJsonPrimitive().getAsString().getBytes();
			ByteArrayInputStream baos = new ByteArrayInputStream(Base64.decodeBase64(stringBytes));
			try {
				return new WavClip(baos);
			} catch (IOException e) {
				e.printStackTrace();
				return null;
			} catch (UnsupportedAudioFileException e) {
				e.printStackTrace();
				return null;
			}
		}	
	}
	
	
	public static class ExtendedResourceAdapter implements 
		JsonDeserializer<ExtendedResource<?>>,
		JsonSerializer<ExtendedResource<?>>{
		@Override
		public ExtendedResource<?> deserialize(JsonElement elem, Type type,
				JsonDeserializationContext context) throws JsonParseException {

			JsonObject jObj = elem.getAsJsonObject();
			final String classTag = jObj.get(CLAZZ_TAG).getAsString();
			jObj.remove(CLAZZ_TAG);
			
			Class<?> clazz = tag2ClazzMap.get(classTag);
			if(clazz==null){
				try {
					clazz = Class.forName(classTag);
				} catch (ClassNotFoundException e) {
					e.printStackTrace();
				}
			}			
			if(clazz == null){
				throw new IllegalStateException("No class enrolled for tag '" + classTag +"'" );
			}
			return context.deserialize(jObj, clazz);
		}

		@Override
		public JsonElement serialize(ExtendedResource<?> res, Type type,
				JsonSerializationContext context) {
//			System.out.println("Extended CALLED!");
//			System.out.println(res.getInterfaceClass());
//			Class<?> if_clazz = res.getInterfaceClass();
			
			JsonElement element = context.serialize(res);
			JsonObject obj = element.getAsJsonObject();
			obj.addProperty(CLAZZ_TAG, res.getClass().getSimpleName());
			obj.addProperty(HASH_TAG, res.hashCode());
			return element;
		}
	}
	
	
	
	@Override
	public String toJson(Object obj){
		if(obj instanceof ExtendedResource){
			return gson.toJson(obj, ExtendedResource.class);
		}
		return gson.toJson(obj);
	}
	
	@Override
	public String toJson(Object obj, Class<?> clazz){
		return gson.toJson(obj, clazz);
	}


	@Override
	public <T> T fromJson(Class<T> clazz, String json){
		if(ExtendedResource.class.isAssignableFrom(clazz)){
			Object obj = gson.fromJson(json, ExtendedResource.class);
			return clazz.cast(obj);
		}
		return gson.fromJson(json, clazz);
	}
	
	
	public static void main(String []args) throws Exception{
//		System.out.println("Test");
		SimpleJsonAdapter adapter = new SimpleJsonAdapter(false/*use refs*/);
//		WavClip wav = new WavClip("/home/kinoko/Dropbox/eclipse-projects/pinsound/pinsound/io/src/test/resources/info/pinlab/pinsound/io/sample.wav");
//		String json = null;
//		
////		json = adapter.toJson(new TextDisplay("hello")); 
//		json = adapter.toJson(new AudioDisplay(wav)); 
//		
////		String string = adapter.toJson(wav);
//		System.out.println(json);
	}

	@Override
	public String getTag4Clazz(Class<?> clazz){
		return clazz2TagMap.get(clazz);
	}
	
}
